javascript - 如何控制 requestAnimationFrame 的波速

标签 javascript jquery canvas

我必须为波浪创建动画。我需要根据数据的可用性来控制波速。是否可以加快波浪速度。我使用 Canvas 来绘制波浪。

提前致谢

fiddle :https://jsfiddle.net/Chaitanya_Kumar/6ztr0Lfh/

function animate() {

            if (x > data.length - 1) {
                return;
            }

            if (continueAnimation) {
                requestAnimationFrame(animate);
            }

            if (x++ < panAtX) {

                var temp = data[x];
                var final = constant-(temp);
                ctx.fillRect(x, final, 1, 1);
                ctx.lineTo(x, final);
                ctx.stroke();
            } else {

                ctx.clearRect(0, 0, canvas.width, canvas.height);
                                    ctx.beginPath();  // reset the path


                for (var xx = 0; xx < panAtX; xx++) {
                    var y = data[x - panAtX + xx];
                    var final = constant - (y);
                    ctx.fillRect(xx, final, 1, 1);
                    ctx.lineTo(xx, final);                        
                }
                ctx.stroke();
            }
        }

最佳答案

子采样数据

下面是数据采样的示例。它使用线性插值对数据源进行二次采样,并在滚动图形显示上显示该数据。

定期间隔数据。

您的问题和 fiddle 中的数据表明您有一个恒定的采样率间隔,并且您希望改变该数据的显示速率。这就是我在下面的演示中所做的。

关于演示

该图表是数据的实时显示,其从左到右的速度取决于您调用示例函数的速率。

  displayBuffer.readFrom(dataSource, dataSpeed, samplesPerFrame)

displayBuffer 是保存可显示数据的对象 dataSource 是数据源,具有 readseek 函数以及 readPos 您寻找的位置 dataSource.seek(0.01); 向前移动0.01个数据样本,然后读取数据dataSource.read();,返回线性插值。

这允许您加快或减慢来自源数据的数据流。

数据读取器对象

//------------------------------------------------------------------------------
// data reader reads from a data source
const dataReader = {
    readPos : 0,
    seek(amount){  // moves read pos forward or back
        if(this.data.length === 0){
            this.readPos = 0;
            return 0;
        }
        this.readPos += amount;
        this.readPos = this.readPos < 0 ? 0 :this.readPos >= this.data.length ? this.data.length - 1 : this.readPos;
        return this.readPos;
    },     
    // this function reads the data at read pos. It is a linear interpolation of the
    // data and does nor repressent what the actual data may be at fractional read positions
    read(){
        var fraction = this.readPos % 1;
        var whole = Math.floor(this.readPos);
        var v1 = this.data[Math.min(this.data.length-1,whole)];
        var v2 = this.data[Math.min(this.data.length-1,whole + 1)];
        return (v2 - v1) * fraction + v1;
    },
}

带时间戳的数据源。

可以通过添加到 dataReader 来调整演示。

如果您的数据采样率不规则,则需要为每个样本添加时间戳。然后,您添加一个类似于seek 的timeSeek 函数,但使用时间样本之间的斜率来计算给定时间的读取位置。它将需要从当前采样时间到下一个采样时间(在寻道方向)对每个样本进行采样,从而使寻道所需的 CPU 周期不确定。

下面是一个 seekTime 示例,它查找由 timeShift 参数向前移动的时间的 readPos(来自上面的 dataReader 对象)。对象的 readTimereadPos 属性已更新,下一个 read() 调用将返回 dataSource.readTime 处的数据>.

    readTime : 0, // current seeked time
    seekTime(timeShift){  // Example is forward seek only
        if(this.timeStamps.length === 0){
            this.readPos = 0;
            return 0;
        }
        this.readTime += timeShift; // set new seeked time
        var readPos = Math.floor(this.readPos);
        // move read pos forward until at correct sample 
        while(this.timeStamps[readPos] > this.readTime &&
                readPos++ < this.timeStamps.length);

        // Warning you could be past end of buffer
        // you will need to check and set seek time to the last
        // timestamp value and exit. Code below the following line
        // will crash if you dont vet here.
        //if(readPos === this.timeStamps.length)


        // now readPos points to the first timeStamp less than the needed
        // time position. The next read position should be a time ahead of the 
        // needed time
        var t1 = this.timeStamps[readPos]; // time befor seekTime
        var t2 = this.timeStamps[readPos+1]; // time after seekTime
        // warning divide by zero if data bad
        var fraction = (this.readTime-t1)/(t2-t1)); // get the sub sample fractional location for required time.
        this.readPos = readPos + fraction;
        return this.readPos;
    },     

警告我忽略了所有安全检查。您应该检查缓冲区结束、错误的时移值。如果带时间戳的数据有错误的样本,您将得到除以零的结果,这将使 dataReader 从该点开始仅返回 NaN 并抛出任何读取错误。因此,为了安全起见,请进行 vert 检查。

注意为了使上述时间戳功能正常工作,您需要确保每个数据样本都有相应的时间戳。如果每个样本没有一对一匹配的时间戳,则上述代码将不起作用。

dataDisplay的更改很简单。只需更改函数中的seek调用即可 dataDisplay.readFrom(dataSource,speed,samples)dataSource.seekTime(speed/samples)speed 现在表示时间而不是样本。 (或者如果我有时间戳,我只是用 seekTime() 覆盖 seek() 函数)这允许 dataDisplay 对象处理 timeStamped和定期间隔数据。

演示

该示例对随机数据进行采样,并以不同的速度和采样率显示它。使用左、右设置显示速度。帧速率约为 60fps,但您可以根据帧之间的时间调整速度变量。

var ctx = canvas.getContext("2d");
window.focus();
//==============================================================================
// the current data read speed
var dataSpeed = 1;
var samplesPerFrame = 1;
requestAnimationFrame(mainLoop);  // start animation when code has been parsed and executed

//------------------------------------------------------------------------------
// data reader reads from a data source
const dataReader = {
    readPos : 0,
    seek(amount){  // moves read pos forward or back
        if(this.data.length === 0){
            this.readPos = 0;
            return 0;
        }
        this.readPos += amount;
        this.readPos = this.readPos < 0 ? 0 :this.readPos >= this.data.length ? this.data.length - 1 : this.readPos;
        return this.readPos;
    },     
    // this function reads the data at read pos. It is a linear interpolation of the
    // data and does nor repressent what the actual data may be at fractional read positions
    read(){
        var fraction = this.readPos % 1;
        var whole = Math.floor(this.readPos);
        var v1 = this.data[Math.min(this.data.length-1,whole)];
        var v2 = this.data[Math.min(this.data.length-1,whole + 1)];
        return (v2 - v1) * fraction + v1;
    },
}

//------------------------------------------------------------------------------
// Create a data source and add a dataReader to it
const dataSource = Object.assign({
        data : [],
    },dataReader
);
// fill the data source with random data
for(let i = 0; i < 100000; i++ ){
    // because random data looks the same if sampled every 1000 or 1 unit I have added
    // two waves to the data that will show up when sampling at high rates
    var wave = Math.sin(i / 10000) * 0.5;
    wave += Math.sin(i / 1000) * 0.5;
    // high frequency data shift
    var smallWave = Math.sin(i / 100) * (canvas.height / 5);
    // get a gaussian distributed random value
    dataSource.data[i] = Math.floor(smallWave + ((wave + Math.random()+Math.random()+Math.random()+Math.random()+Math.random()) / 5) *  canvas.height);
}

//------------------------------------------------------------------------------
// Data displayer used to display a data source  
const dataDisplay = {
    writePos : 0,
    width : 0,
    color : "black",
    lineWidth : 1,
    // this function sets the display width which limits the data buffer
    // when it is called all buffers are reset
    setDisplayWidth(width){
        this.data.length = 0;
        this.width = width;
        this.writePos = 0;
        if(this.lastRead === undefined){
            this.lastRead = {};
        }
        this.lastRead.mean = 0;
        this.lastRead.max = 0;
        this.lastRead.min = 0;
    },
    // this draws the buffered data scrolling from left to right
    draw(){
        var data = this.data; // to save my self from writing this a zillion times
        const ch = canvas.height / 2;
        if(data.length > 0){  // only if there is something to draw
            ctx.beginPath();
            ctx.lineWidth = this.lineWidth;
            ctx.strokeStyle = this.color;
            ctx.lineJoin = "round";
            if(data.length < this.width){  // when buffer is first filling draw from start
                ctx.moveTo(0, data[0])
                for(var i = 1; i < data.length; i++){
                    ctx.lineTo(i, data[i])
                }
            }else{  // buffer is full and write position is chasing the tail end
                ctx.moveTo(0, data[this.writePos])
                for(var i = 1; i < data.length; i++){
                    ctx.lineTo(i, data[(this.writePos + i) % data.length]);
                }
            }
            ctx.stroke();
        }
    },
    // this reads data from a data source (that has dataReader functionality)
    // Speed is in data units, 
    // samples is number of samples per buffer write.
    //         samples is only usefull if speed > 1 and lets you see the
    //         mean, min, and max of the data over the speed unit
    //         If speed < 1 and sample > 1 the data is just a linear interpolation 
    //         so the lastRead statistics are meaningless (sort of)
    readFrom(dataSource,speed,samples){ // samples must be a whole positive number
        samples = Math.floor(samples);
        var value = 0;
        var dataRead;
        var min;
        var max;
        for(var i = 0; i < samples; i ++){  // read samples
            dataSource.seek(speed / samples);  // seek to next sample
            dataRead = dataSource.read();     // read the sample
            if(i === 0){
                min = dataRead;
                max = dataRead;
            }else{
                min = Math.min(dataRead,min);
                max = Math.min(dataRead,max);
            }
            value += dataRead;
        }
        // write the samples data and statistics.
        this.lastRead.min = min;
        this.lastRead.max = max;
        this.lastRead.delta = value / samples - this.lastRead.mean;
        this.lastRead.mean = value / samples;
        this.data[this.writePos] = value / samples;
        this.writePos += 1;
        this.writePos %= this.width;
    }
}
// display data buffer
var displayBuffer = Object.assign({ // this data is displayed at 1 pixel per frame
        data : [],                 // but data is written into it at a variable speed
    },
    dataDisplay // add display functionality
);

//------------------------------------------------------------------------------
// for control
const keys = {
    ArrowLeft : false,
    ArrowRight : false,
    ArrowUp : false,
    ArrowDown : false,
}
function keyEvent(event){
    if(keys[event.code] !== undefined){
        event.preventDefault();
        keys[event.code] = true;
    }
}
addEventListener("keydown",keyEvent);

//------------------------------------------------------------------------------
function mainLoop(time){
    ctx.clearRect(0,0,canvas.width,canvas.height);
    if(canvas.width !== displayBuffer.width){
        displayBuffer.setDisplayWidth(canvas.width);
    }
    displayBuffer.readFrom(dataSource,dataSpeed,samplesPerFrame);
    displayBuffer.draw();
    
    
    //-----------------------------------------------------------------------------
    // rest is display UI and stuff like that
    ctx.font = "16px verdana";
    ctx.fillStyle = "black";
    //var dataValue =displayBuffer.lastRead.mean.toFixed(2);
    //var delta = displayBuffer.lastRead.delta.toFixed(4);
    var readPos = dataSource.readPos.toFixed(4);
    //if(displayBuffer.lastRead.delta > 0){ delta = "+" + delta }
    // ctx.fillText("Data : " + dataValue + " ( " +delta +" )" ,4,18);
    ctx.setTransform(0.9,0,0,0.89,4,18);
    ctx.fillText("Speed : " + dataSpeed.toFixed(3) + ", Sample rate :" +samplesPerFrame + ", Read @ "+readPos ,0,0);
    ctx.setTransform(0.7,0,0,0.7,4,32);
    if(samplesPerFrame === 1){
        ctx.fillText("Keyboard speed -left, +right Sample rate +up",0,0);
    }else{
        ctx.fillText("Keyboard speed -left, +right Sample rate -down, +up",0,0);
    }
    ctx.setTransform(1,0,0,1,0,0);
    if(keys.ArrowLeft){
        keys.ArrowLeft = false;
        if(dataSpeed > 1){
            dataSpeed -= 1;
        }else{
            dataSpeed *= 1/1.2;
        }
    }
    if(keys.ArrowRight){
        keys.ArrowRight = false;
        if(dataSpeed >= 1){
            dataSpeed += 1;
        }else{
            dataSpeed *= 1.2;
            if(dataSpeed > 1){ dataSpeed = 1 }
        }
    }
    if(keys.ArrowUp){
        keys.ArrowUp = false;
        samplesPerFrame += 1;
    }
    if(keys.ArrowDown){
        keys.ArrowDown = false;
        samplesPerFrame -= 1;
        samplesPerFrame  = samplesPerFrame < 1 ? 1 : samplesPerFrame;
    }

    requestAnimationFrame(mainLoop);
}
canvas {
   border : 2px black solid;
}
<canvas id=canvas width=512 height=200></canvas>

这种方式读取和显示数据既快速又简单。很容易向数据源添加网格标记和数据处理并显示数据。演示(定期间隔数据)可以轻松处理显示大型数据源,同时放大和缩小数据。请注意,对于带有时间戳的数据,上述seekTime函数不适合大型数据集。您需要对此类数据建立索引以获得更有效的查找时间。

关于javascript - 如何控制 requestAnimationFrame 的波速,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43717611/

相关文章:

javascript - 从 Canvas 中删除饼图并替换为另一个饼图

javascript - 需要改进此脚本

javascript - 如何从同一目录中的 JavaScript 文件导入 JavaScript 函数?

javascript - 将 1 个表单附加到另一个表单 - 然后提交

javascript - 如果至少选中了一个复选框,是否有一种方法可以启用按钮?

javascript - JQuery 变量没有转移到 JavaScript

html - Google Zeitgeist 2011 可视化

javascript - 配置注释错误 : Argument 'fn' is not a function, 得到字符串

javascript - 具有 2 个不同长度的数组?

javascript - 在 KineticJS 中检测舞台上的点击而不是形状上的点击