Home > other >  Easing the scaling of a line in Javascript
Easing the scaling of a line in Javascript

Time:02-26

I am trying to scale a line along the dot product in X between its start and end point in Javascript with an ease that goes from 0 to 1. I am seeking something like the red line in this image: Scaled with Ease

I have the linear scale working, but I'm not sure how to ease the scale. I have tried applying the ease to the linear points then subtracting that amount from the linear amount, but it's not right: the eased points surpass the start point. Running the snippet below should demonstrate the issue. Could anyone please help me get this working? I would very greatly appreciate it.

function magnitude(p) {
  return Math.sqrt(p.x ** 2   p.y ** 2);
}

function normalize(p) {
  let m = magnitude(p);
  if (m > 0) {
    return { x: p.x / m, y: p.y / m };
  }
}

function dot(pt1, pt2) {
  return pt1.x * pt2.x   pt1.y * pt2.y;
}

//ease functions
function easeInQuad(n) {
  return n ** 2;
}

function easeOutQuad(n) {
  return -n * (n - 2);
}

function render_line_graph() {
  let points = [
    { x: 2, y: 2 },
    { x: 3, y: 1 },
    { x: 4, y: 4 },
    { x: 5, y: 3 },
    { x: 6, y: 6 },
    { x: 7, y: 5 },
    { x: 8, y: 6 }
  ];

  let scale_amount = 1-slider.value/100;
  
  //first & last points x,y respectively
  let x1 = points[0].x;
  let y1 = points[0].y;
  let x2 = points.slice(-1)[0].x;
  let y2 = points.slice(-1)[0].y;

  //distance between the first & last points' coordinates
  let x_dist = x2 - x1;
  let y_dist = y2 - y1;
  let vec = { x: x2 - x1, y: y2 - y1 };
  let line = normalize(vec);
  let normal_data = [];
  let linear_data = [];
  let ease_data = [];
  let chart = null;

  for (let i = 0; i < points.length; i  ) {
    let p = points[i];
    let dot_product = dot({ x: p.x - x1, y: p.y - y1 }, line);

    if (normal_check.checked)
    {
      normal_data.push({ x: p.x, y: p.y });
    }

    if (linear_check.checked)
    {
      let linear_x = p.x - dot_product * scale_amount * line.x;
      let linear_y = p.y - dot_product * scale_amount * line.y;
      linear_data.push({ x: linear_x, y: linear_y });
    }

    if (ease_check.checked)
    {
      let alpha = dot({ x: p.x - x1, y: p.y - y1 }, line) / magnitude(vec);
      let alpha_eased = 1 - easeInQuad(1 - alpha);

      let ease_x = p.x - alpha_eased * scale_amount * vec.x;
      let ease_y = p.y - alpha_eased * scale_amount * vec.y;
      ease_data.push({ x: ease_x, y: ease_y });
    }
  }
  
  chart = new CanvasJS.Chart("chartContainer", {
    animationEnabled: false,
    theme: "light2",
    title: {
      text: "Scaling a Line with Ease"
    },
    data: [
      {
        type: "line",
        color: "blue",
        lineDashType: "dash",
        indexLabelFontSize: 16,
        dataPoints: [points[0],points.slice(-1)[0]]
      },
      {
        type: "line",
        color: "blue",
        indexLabelFontSize: 16,
        dataPoints: normal_data
      },
      {
        type: "line",
        color: "green",
        indexLabelFontSize: 16,
        dataPoints: linear_data
      },
      {
        type: "line",
        color: "red",
        indexLabelFontSize: 16,
        dataPoints: ease_data
      },
    ]
  });
  chart.render();
}

var slider = document.getElementById("myRange");
var output = document.getElementById("demo");
var normal_check = document.getElementById("normal");
var linear_check = document.getElementById("linear");
var ease_check = document.getElementById("ease");

output.innerHTML = slider.value   "%"; // Display the default slider value
render_line_graph();

slider.oninput = function () {
  scale_amount = this.value / 100;
  output.innerHTML = this.value   "%";
  render_line_graph();
};

normal_check.oninput = function () {
  render_line_graph();
};

linear_check.oninput = function () {
  render_line_graph();
};

ease_check.oninput = function () {
  render_line_graph();
};
html
{
  font-family: sans-serif;
}


#controls
{
  display: flex;
  flex-direction: column;
  border: 1px solid #ddd;
  padding: 20px;
  width: fit-content;
  position: absolute;
  top: 0;
  right: 0;
}
<script src="https://canvasjs.com/assets/script/canvasjs.min.js"></script>
<div id="chartContainer" style="height: 180px; width: 320px;"></div>
<div id="controls">
  <div><input type="checkbox" id="normal" name="normal" value="True" checked>
    <label for="normal"> Normal Line</label>
  </div>
  <div><input type="checkbox" id="linear" name="linear" value="True" checked>
    <label for="linear"> Linear Scale</label>
  </div>
  <div><input type="checkbox" id="ease" name="ease" value="True" checked>
    <label for="ease"> Scale with Ease</label>
  </div>
  <div >
    <input type="range" min="0" max="100" value="50"  id="myRange">
    <p>Scale: <span id="demo"></span></p>
  </div>
</div>

CodePudding user response:

Let's consider what this code is supposed to achieve: we're not really "scaling" so much as "repositioning points, parallel to the trend line". While we can treat that as scaling x coordinates, we're definitely not scaling y: instead, we want to preserve the height of the point above (or below) the trendline, so let's write that up.

First, we capture the trend line data:

const first = points[0];
const last = points.slice(-1)[0];
const d = {
  x: last.x - first.x,
  y: last.y - first.y
};

With that information, we can now represent each point as not so much lying at some value x but at some value start.x r * d.x where r is 0 for the first coordinate, and 1 for the last coordinate:

function getXratio(p) {
  let cx = p.x - first.x;
  return cx / d.x;
}

And now we can express coordinates in terms of those ratios: each point has an x coordinate that we can represent as first.x ratio * d.x, and a y coordinate that we can represent as "the trend line height at that x, plus some height offset h above or below the trend line": first.y ratio * d.y h.

Now the trick: if we want to get scaling as you're showing, we can introduce our scaling factor, let's call it factor, as:

function rewritePoint(p, factor) {
  let ratio = getXratio(p);

  // scale x "as normal"
  let x = first.x   factor * d.x;

  // don't scale y, but _reposition_ it along the trend line instead.
  let h = p.y - (first.y   ratio * d.y);
  let y = first.y   factor * ratio * d.y   h;

  return { x, y };
}

That is, we preserve the height above/below the trend line h, and we add it to the height of the trend line based on the scaled x coordinate.

If we just do that, we end up with plain, linear scaling. So let's add a mapping function that lets rewritePoint mess with the ratio value:

function rewritePoint(p, factor, mapRatio) {
  let ratio = getXratio(p);

  // scale x "as normal", corrected for mapping
  let x = first.x   factor * mapRatio(ratio) * d.x;

  // reposition y along the trend line, using height
  // above/below the trend at the original x position,
  // adjusted for the mapped x position.
  let t = first.y   ratio * d.y;
  let h = p.y - t;
  let y = first.y   h   factor * mapRatio(ratio) * d.y;

  return { x, y };
}

function linear(p, factor) {
  return rewritePoint(p, factor, v => v);
}

const easingFactor = 3;

function easeIn(p, factor) {
  return rewritePoint(p, factor, v => v ** (1/easingFactor));
} 

function easeOut(p, factor) {
  return rewritePoint(p, factor, v => v ** easingFactor);
}

There are of course "nicer" formulae that you can use for easing, but these are about as basic as you can get while getting the point across. So, putting that all together, with plain data in blue, linear compressed in red, eased in using green, and eased out using purple:

const points = [
  { x: 2, y: 2 },
  { x: 3, y: 1 },
  { x: 4, y: 4 },
  { x: 5, y: 3 },
  { x: 6, y: 6 },
  { x: 7, y: 5 },
  { x: 8, y: 6 }
];

const first = points[0];
const last = points.slice(-1)[0];
const d = {
  x: last.x - first.x,
  y: last.y - first.y
};

function getXratio(p) {
  let cx = p.x - first.x;
  return cx / d.x;
}

function rewritePoint(p, factor, mapRatio) {
  let ratio = getXratio(p);

  // scale x "as normal"
  let x = first.x   factor * mapRatio(ratio) * d.x;

  // don't scale y, but _reposition_ it along the trend line instead.
  let t = first.y   ratio * d.y;
  let h = p.y - t;
  let y = first.y   h   factor * mapRatio(ratio) * d.y;

  return { x, y };
}

function linear(p, factor) {
  return rewritePoint(p, factor, v => v);
}

const easingFactor = 2.2;

function easeIn(p, factor) {
  return rewritePoint(p, factor, v => v ** (1/easingFactor));
} 

function easeOut(p, factor) {
  return rewritePoint(p, factor, v => v ** easingFactor);
}

function drawChart() {
  let factor = slider.value/100;

  let scaled = points.map(p => linear(p, factor));
  let easedIn = points.map(p => easeIn(p, factor));
  let easedOut = points.map(p => easeOut(p, factor));

  chart = new CanvasJS.Chart("chartContainer", {
    animationEnabled: false,
    theme: "light2",
    data: [
      {
        type: "line",
        color: "blue",
        lineDashType: "dash",
        indexLabelFontSize: 16,
        dataPoints: [points[0],points.slice(-1)[0]]
      },
      {
        type: "line",
        color: "blue",
        indexLabelFontSize: 16,
        dataPoints: points
      },
      {
        type: "line",
        color: "red",
        indexLabelFontSize: 16,
        dataPoints: scaled 
      },
      {
        type: "line",
        color: "green",
        indexLabelFontSize: 16,
        dataPoints: easedIn
      },
      {
        type: "line",
        color: "purple",
        indexLabelFontSize: 16,
        dataPoints: easedOut
      }
    ]
  });
  chart.render();
}

slider.addEventListener(`input`, drawChart);

drawChart();
#chartContainer { height: 180px; width: 320px; }
<script src="https://canvasjs.com/assets/script/canvasjs.min.js"></script>
<div id="chartContainer"></div>
<input type="range" min="0" max="100" value="100" id="slider">

  • Related