Search code examples
javascriptuser-interfacescaleaxes

Proportionally scale x and y coordinates in Javascript


I'm trying to build a control like the following:

enter image description here enter image description here

I have it working when it is unlinked, but I can't figure out how to scale it by whatever value is entered in either the first or second input field.

class OffsetControl extends Component {
    constructor(props) {
      super(props)
      this.state = {
        axes: { x: 0, y: 0 },
        isLinked: false
      }
      this._handleAxisChange = this._handleAxisChange.bind(this)
    }

    _scale(axes) {
      const { axes: { x, y } } = this.state
      let ratio = [axes.x / x, axes.y / y ]

      ratio = Math.min(ratio[0], ratio[1])

      this.setState({
        axes: {
          x: x * ratio,
          y: y * ratio
        }
      })
    }

    _handleAxisChange({ target: { name, value } }) {
      const { isLinked, axes } = this.state
      const newAxes = { ...axes, [name]: +value }
      if (isLinked) {
        this._scale(newAxes)
      } else {
        this.setState(newAxes)
      }
    }

    render() {
      const { axes: { x, y }, isLinked } = this.state
      return (
        <div>
          <input type="number" name="x" value={x} onChange={this._handleAxisChange}/>
          <button onClick={() => this.setState({ isLinked: !isLinked })}>
            { isLinked ? 'Unlink' : 'Link' }
          </button>
          <input type="number" name="y" value={y} onChange={this._handleAxisChange}/>
        </div>
      )
    }
  }

You can find a live version here. Any help is greatly appreciated.


Solution

  • It is basically the straight line formula:

    y = mx + c
    

    In the general case (like converting cm to inches) c is zero. So the formula is merely:

    y = mx
    

    You only need the c in cases where you have an offset (like converting celsius to fahrenheit).

    How to apply this to linked scaling?

    Just find out the m (or if you're more familiar with calculus, dy/dx - which is the terminology I'll be using in the following code):

    var current_input = 5;
    var current_output = 9;
    
    var dy_dx = current_output/current_input;
    
    var new_output = dy_dx * new_input;
    

    So, a concrete example:

    current_input = 5;
    current_output = 9;
    
    // change 5 to 11, what should 9 change to?
    
    new_output = (9/5) * 11; // result is 19.8
    

    You can flip the equation around if you need to calculate the first value if you change the second value:

    current_input = 9;
    current_output = 5;
    
    // change 9 to 15, what should 5 change to?
    
    new_output = (5/9) * 15; // result is 8.333
    

    In general you can implement it as:

    function scale (old_input, old_output, new_input) {
        return (old_output/old_input) * new_input;
    }
    

    Although numerically it is better to store the value of m so that you don't lose accuracy after doing lots of calculations:

    function Scaler (x,y) {
        this.m = y/x;
    }
    
    Scaler.prototype.calculate_y (new_x) {
        return this.m * new_x;
    }
    
    Scaler.prototype.calculate_x (new_y) {
        return (1/this.m) * new_y;
    }
    
    // so you can do:
    
    var scaler = new Scaler(5,9);
    var new_output = scaler.calculate_y(11);
    

    Highschool math is useful after all.