1. 42
  1.  

  2. 4

    One function I found useful to make smooth but snappy slider UIs and such is “detent”

    function detent(n, detent, grid, snap) {
      // This function is useful to implement smooth transitions and snapping.
      // Map all values that are within detent/2 of any multiple of grid to
      // that multiple. Otherwise, if snap is true, return self, meaning that
      // the values in the dead zone will never be returned. If snap is
      // false, then expand the range between dead zone so that it covers the
      // range between multiples of the grid, and scale the value by that
      // factor.
      var r1 = roundTo(n, grid); // Nearest multiple of grid
      if (Math.abs(n - r1) < detent / 2) return r1; // Snap to that multiple...
      if (snap) return n // ...and return n
      // or compute nearest end of dead zone
      var r2 = n < r1 ? r1 - (detent / 2) : r1 + (detent / 2);
      // and scale values between dead zones to fill range between multiples
      return r1 + ((n - r2) * grid / (grid - detent));
    }
    

    (that’s from lively.lang, MIT licensed)

    Here is an example of a free-form rotation that still allows stickiness at 45 degree angles: https://twitter.com/robertkrahn/status/1403102487717023746 The gif doesn’t really transport it but it just feels “smooth”.

    I’ve first seen it in Squeak/Smalltalk where it was added by Dan Ingalls in 1998 but don’t know what the history behind it is. It might have been part of earlier Smalltalks. For completeness, here is the Smalltalk version:

    detentBy: detent atMultiplesOf: grid snap: snap
       "Map all values that are within detent/2 of any multiple of grid to that multiple.  Otherwise, if snap is true, return self, meaning that the values in the dead zone will never be returned.  If snap is false, then expand the range between dead zones so that it covers the range between multiples of the grid, and scale the value by that factor."
       | r1 r2 |
       r1 := self roundTo: grid.  "Nearest multiple of grid"
       (self roundTo: detent) = r1 ifTrue: [^ r1].  "Snap to that multiple..."
       snap ifTrue: [^ self].  "...or return self"
    
       r2 := self < r1  "Nearest end of dead zone"
           ifTrue: [r1 - (detent asFloat/2)]
           ifFalse: [r1 + (detent asFloat/2)].
       "Scale values between dead zones to fill range between multiples"
       ^ r1 + ((self - r2) * grid asFloat / (grid - detent))
    

    (that version is from Squeak 5.3, MIT licensed with the changeset indicating di 2/19/98 21:58 as the author).

    1. 1

      Could you provide some more information about detent? I have tried to find something, but without any luck

      1. 4

        In essence it’s just a way to provide a free-form choice of values in a certain range while also allow snapping to certain multiples /without/ disallowing certain values (e.g. those that are close to multiples).

        Here are two plots:

        (and sorry for the links to twitter but I dunno how to share images otherwise)

    2. 3

      This is a nice introduction to the concepts and a good set of examples that explains what is happening.

      You can use matrix algebra if you want to generalize it to (1) multiple points at the same time, (2) more dimensions, and/or (3) more general transformations.

      The idea is that you can represent the “rescaling” as a matrix and then use matrix multiplication to calculate the transformed point.

      For example, to translate any value in interval (a, b) to (0, 1), you can use the following matrix:

      T = [-a/b-a, 0; 0, 1/b-a] and use the point A = [1; x] as input.

      You can then start to “string” together multiple transformations by multiplying together the transformation matrices (for example, the transformation that you discuss is the combination of a translation of -a and a dilation of factor 1/(b-a)) or adding multiple transformed points by adding columns to the input matrix.

      You can also extend this to multiple dimensions (e.g., 2D) by adding rows and columns to the matrix and an additional row to the point. The basic concepts will still work out as in the 1D case.

      Note: I am not 100% sure of my math. Someone check it. :)

      1. 2

        You can use matrix algebra if you want to generalize it to (1) multiple points at the same time, (2) more dimensions, and/or (3) more general transformations.

        Can you do all three of these at the same time with matrices or do you need to generalise to something with more numbers in it?

      2. 2

        Thought this was going to be about the infamous fast inverse square root formula https://en.wikipedia.org/wiki/Fast_inverse_square_root