优化钟摆动画(javascript)

Optimize pendulum animation (javascript)

TL;DR:我写了基于情节的 javascript 数学摆模拟。它工作起来很慢。我正在寻找有关如何优化它的想法。目前正在尝试 "bare" d3.js 并为 SVG 的坐标与我自己的逻辑坐标之间的坐标转换问题而苦苦挣扎。


我正在编写关于常微分方程的网络教科书,希望包括数学摆的交互式模拟和可视化。可视化应包含摆本身、其势能图和全能等值线图。然后用户可以通过点击能量等值线图来选择初始条件,然后动画应该开始显示该点如何在相位 space.

中移动

我写了一个这样模拟的例子:

https://jsfiddle.net/ischurov/p1krqnt6/

它是基于情节的。我创建了三个轴并在其中放置了必要的图表。表示系统当前状态的点也是 plotly 中的图形(即只有一个点的散点图)。

动画的工作原理如下:我得到阶段space中点的当前坐标,一段时间后计算这个点的新位置,然后根据这个新位置更新我的图形。对应代码:

var div = document.getElementById('myDiv');

function updateState(phi, v) {
        var update = {x: [[phi], [phi], [0, Math.sin(phi)]], y: [[v],
            [PotentialEnergy(phi)], [0, -Math.cos(phi)]]};
        Plotly.restyle(div, update, [phaseDotIndex, 3, 4]);
}

myPlot.on('plotly_click', function(data){
    if(data.points[0].data.type == 'contour'){
        updateState(data.points[0].x, data.points[0].y);
    }
});

var animate = null;
$('.animate_button').click(function(){
    var div = document.getElementById('myDiv');
    if(animate === null) {
        var phi = div.data[phaseDotIndex].x[0],
            v = div.data[phaseDotIndex].y[0], 
            E = FullEnergy(phi, v);
        animate = setInterval(function() {
            var phi = div.data[phaseDotIndex].x[0],
                v = div.data[phaseDotIndex].y[0],
                step = 0.1, newphi, newv, update;

            newphi = phi + v * step;
            newv = v + step * Force(phi);

            /* skip some tweaks here */

            updateState(phi, v);
        }, 
        100)
    }
    else
    {
        clearInterval(animate);
        animate = null;
    }
}

此代码几乎按预期工作,但确实很慢且不流畅 — 至少在 Firefox 下是这样(如果我减少更新间隔,它会工作得更糟)。

我正在寻找优化方法。

我认为性能问题是由于 plotly 的更新过程造成的:为了移动一个点,它必须重新计算整个图片,而且速度很慢。所以我正在寻找以不同方式做到这一点的方法。

有什么想法吗?

我正在寻找一些可以更快的直接 d3.js 方法。我在这里看到以下步骤:

  1. 画出势能图和全能等高线图
  2. 画出钟摆本身。
  3. 在势能图和等高线图上画小圆圈。
  4. 创建 'onclick' 事件处理程序以允许用户选择初始状态。
  5. 运行 通过根据当前状态更新圆圈和钟摆的位置来循环动画。

要继续第 1 步,我可以使用第三方 d3.js 库,例如 conrec for contour plots and/or excellent maurizzzio's function plot 甚至 plotly 本身(但我不打算使用 plotly 来更新图表)。第2步似乎是可行的,但我还没有尝试过。目前最困难的是第 3 步和第 4 步,因为我不明白如何将 SVG 坐标转换为我的图形坐标(用某些库绘制),反之亦然。

或者也许有更简单的方法来做到这一点?

我是 function plot which is built on top of d3, luckily d3 has methods to perform mappings in d3-scale 的作者,所以假设你有一个 width x height 维度的 canvas 应该线性映射到二维欧氏矩形 [xMin, yMin] x [xMax, yMax] space 你需要创建两个线性比例尺

var xScale = d3.scale.linear()
  .domain([xMin, xMax])
  .range([0, width])

var yScale = d3.scale.linear()
  .domain([yMin, yMax])
  .range([height, 0])

请注意,在 SVG 中,y 轴被翻转,因此 yScale 范围也被翻转,然后任何 2D 欧氏点都转换为 SVG 坐标,如下所示

var xCanvas = xScale(point.x)
var yCanvas = yScale(point.y)

逆变换由

给出
var xLogical = xScale.invert(point.x)
var xLogical = yScale.invert(point.y)

我使用上述方法针对您的问题写的一个可能的解决方案是

var instance = functionPlot({
  target: '#demo',
  disableZoom: true,
  data: [{
    fn: 'sin(10*(-cos(x) + y^2/2-1))',
    fnType: 'implicit'
  }]
})
var xScale = instance.meta.xScale
var yScale = instance.meta.yScale
var canvas = instance.canvas

var circle = canvas.append('circle')
  .attr("r", 5)
  .style("fill", "purple")

var start = Date.now()

function animate() {
  
  // move the point along the circle of radius 1
  var t = (Date.now() - start) * 0.003
  var xLogical = Math.cos(t)
  var yLogical = Math.sin(t)
  var xCanvas = xScale(xLogical)
  var yCanvas = yScale(yLogical)
  circle
    .attr('cx', xCanvas)
    .attr('cy', yCanvas)
  
  requestAnimationFrame(animate)
}

animate()
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://maurizzzio.github.io/function-plot/js/function-plot.js"></script>
<div id="demo"></div>

Issue in function plot's GitHub

我知道这不是您想要的,但它确实表明您的代码可以很紧凑并且仍然可以满足您的要求。

以下是一些代码,演示了一些使钟摆动画的数学,从我的一个 javascript 小部件中提取,但数学逻辑应该仍然可以在您自己的项目中使用。

创建一个图像对象 - pendulumSet,一个漂亮的圆球就可以了。

// variables set for the pendulum

var gravity = -0.0110808; // tuned to -0.110808 as it approximates a 1 second interval
var acceleration = 0.1; //0.1
var velocity = 0.18; //.18
var angle = 8; // 8 (.4 radians = 22.91 degrees)

创建一个间隔为 0.01 秒的计时器

将其放入计时器:

acceleration = gravity * angle;
velocity += acceleration;
angle += velocity;
pendulumSet.rotation = angle +180; // rotation as per a widget engine

就是这样...

您可能必须使用类似下面的方法来实现原生 javascript 的旋转:

pendulumSet.style.transform = "rotate(90deg)"; // change the 90deg

上面的代码将以更紧凑的方式模拟钟摆。这是 1/100 秒,图形动画和数学计算,因此需要一些 cpu,这是不可避免的。尽管如此,代码很紧凑,影响很小。根据解释代码的引擎,cpu 的使用量约为。 2009 年 2.5ghz core2duo 的 20-25%,由更现代、更快的 cpus 轻松处理。 运行 Firefox 中的类似代码可能会明显变慢,因为根据我的经验,Firefox 执行类似动画的效率似乎较低。你只需要尝试一下就知道了。

代码取自此示例 javascript 小部件: A steampunk clock for the desktop

因此您可以看到完全相同的代码在运行。