在 Javascript 中简化线条的缩放

Easing the scaling of a line in Javascript

我正在尝试在 Javascript 中的起点和终点之间沿 X 中的点积缩放一条线,从 0 到 1 不等。我正在寻找类似红线的东西图片:

我可以使用线性刻度,但我不确定如何缓和刻度。我尝试将缓动应用于线性点,然后从线性量中减去该量,但这是不对的:缓动点超过了起点。 运行 下面的代码片段应该可以说明这个问题。谁能帮我解决这个问题?我将不胜感激。

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 class="slidecontainer">
    <input type="range" min="0" max="100" value="50" class="slider" id="myRange">
    <p>Scale: <span id="demo"></span></p>
  </div>
</div>

让我们考虑一下这段代码应该实现的目标:我们并不是真正地“缩放”,而是“重新定位点,平行于趋势线”。虽然我们可以将其视为缩放 x 坐标,但我们绝对不会缩放 y:相反,我们希望保留趋势线上方(或下方)点的高度,所以让我们把它写下来.

首先,我们捕捉趋势线数据:

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

有了这些信息,我们现在可以将每个点表示为不是位于某个值 x,而是位于某个值 start.x + r * d.x,其中 r 的第一个坐标为 0,最后一个坐标为 1:

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

现在我们可以用这些比率来表示坐标:每个点都有一个 x 坐标,我们可以表示为 first.x + ratio * d.x,还有一个 y 坐标,我们可以表示作为“x 处的趋势线高度加上趋势线上方或下方的一些高度偏移 h”:first.y + ratio * d.y + h.

现在是诀窍:如果我们想要像您展示的那样进行缩放,我们可以引入我们的缩放因子,我们称之为 factor,如:

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 };
}

也就是我们保留高度above/below趋势线h,我们根据缩放 x 坐标。

如果我们只是这样做,我们最终会得到简单的线性缩放。所以让我们添加一个映射函数,让 rewritePointratio 值混淆:

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);
}

当然有一些“更好”的公式可用于缓动,但这些都是您在理解要点时所能获得的基本知识。因此,将所有内容放在一起,蓝色为纯数据,红色为线性压缩,绿色为缓和,紫色为缓和:

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">