How do I linearly interpolate (lerp) ranged input to ranged output?

2.5k views Asked by At

I want to generically interpolate between a range of input values (say, between A and B) and get a range of output values (say, between C and D). Sometimes I want to clamp the value (so that B+10000 still outputs D) and sometimes I don't. How do I do it?

For example, given input speeds between 20 mph and 80 mph, I want to adjust the zoom level of my map between 17 and 15:

Without Clamping

     | \ 
     |  \
 17  |   \
     |    \
     |     \
     |      \
     |       \
 15  |        \
     |         \
     +----------\-
        20  80   \

With Clamping

     |
 17  |----
     |    \
     |     \
     |      \
     |       \
 15  |        ----
     |
     +------------
        20  80    

I found this utility function, but (a) it does not support clamping by itself, requiring a second function call, and (b) it only supports input between 0 and 1.

1

There are 1 answers

0
Phrogz On BEST ANSWER

The general (unclamped) equation you want is:

var unclamped = (x-minX) * (maxY-minY)/(maxX-minX) + minY;

For clamping, you can either clamp the output after you calculate the result:

var clamped = Math.max( minY, Math.min( maxY, unclamped ) );

Or you can clamp the input before you use it:

x = Math.max( minX, Math.min( maxX, x ) )
var clamped = (x-minX) * (maxY-minY)/(maxX-minX) + minY;

If the slope of the line does not change, and your clamping desires do not change, you can improve performance by pre-calculating it once and generating a function tailored to your input and needs:

// Generate a lerp function for a specific set of input/output,
// with or without clamping to the output range.
function lerp(minX, maxX, minY, maxY, clampFlag) {
  var slope = (maxY-minY)/(maxX-minX);
  return clampFlag ?
    function(x){ return ((x<minX?minX:x>maxX?maxX:x) - minX) * slope + minY }
    :
    function(x){ return (x-minX)*slope + minY }
}

In action:

prepPlotter(); // Get ready to draw a pretty graph of the results

// Make a simple input/output function
var zoomForSpeed = lerp(20, 80, 17, 15, true);

for (var speed=0; speed<=99; speed++) {
  var zoom = zoomForSpeed(speed); // Bam! Lerp'd!
  plot(speed,zoom);               // Proof of the goodness
}

// Show the min/max input and output
ctx.fillStyle = 'red';
plot(20,17,2);
plot(80,15,2);

function plot(speed,zoom,scale) {
  ctx.fillRect(speed,zoom,0.5*(scale||1),0.03*(scale||1));
}

function prepPlotter() {
  ctx = document.querySelector('canvas').getContext('2d');
  ctx.translate(0,875);
  ctx.scale(3,-50);
}

function lerp(minX, maxX, minY, maxY, clampFlag) {
  var slope = (maxY-minY)/(maxX-minX);
  return clampFlag ? function(x){ return ((x<minX?minX:x>maxX?maxX:x) - minX) * slope + minY } : function(x){ return (x-minX)*slope + minY }
}
<canvas>Press "Run code snippet" for a graph of zoomForSpeed</canvas>