将画布制作成类似电视噪声的动画效果

65

我有一个名为generateNoise()的函数,它创建一个画布元素并将随机RGBA值绘制到其中,从而呈现噪点的外观。


我的问题

最好的方法是什么,可以无限地动画化这个噪声,以使其具有更多的生命力?


JSFiddle

function generateNoise(opacity) {
    if(!!!document.createElement('canvas').getContext) {
        return false;
    }
    var canvas = document.createElement('canvas'),
        ctx = canvas.getContext('2d'),
        x,y,
        r,g,b,
        opacity = opacity || .2;

        canvas.width = 55;
        canvas.height = 55;

        for (x = 0; x < canvas.width; x++){
            for (y = 0; y < canvas.height; y++){
                r = Math.floor(Math.random() * 255);
                g = Math.floor(Math.random() * 255);
                b = Math.floor(Math.random() * 255);

                ctx.fillStyle = 'rgba(' + r + ',' + b + ',' + g + ',' + opacity + ')';
                ctx.fillRect(x,y,1,1);

            }
        }
        document.body.style.backgroundImage = "url(" + canvas.toDataURL("image/png") + ")";

}
generateNoise(.8);

1
window.setInterval('generateNoise(.8)',50); - CrayonViolent
1
你可以通过这样做来减少 Math.randomMath.floor 的调用:x = Math.floor(Math.random() * 0xffffff); r = x & 0xff; g = (x & 0xff00) >>> 8; b = (x & 0xff0000) >>> 16; - Paul S.
1
只需要添加 requestAnimationFrame(generateNoise); 就可以实现你所需的功能,将其放在函数的第一行即可。http://jsfiddle.net/eS9cc/ - Mouseroot
可能是因为你正在迭代x,y像素位置,而我正在迭代每个像素并添加4个r,g,b和alpha,我使用255(黑色)作为r,g,b和介于155和254之间的随机值,同时我直接修改像素,而你则放置每个单独的像素。因此,我的循环会更改数组中的所有像素,然后应用它,而你的循环则更改每个像素并设置它。 - Mouseroot
@MatthewHarwood 你想要我的演示还是其他什么? - Ilan Biala
显示剩余8条评论
7个回答

80

更新于2017年1月因为在评论中指出了一些问题,回答变得有点混乱,所以我重写了整个答案。原始答案可以在这里找到。新答案基本上使用相同的代码但进行了改进,并且使用了几种新技术,其中一种利用了自此答案首次发布以来可用的新功能。


要获得“真正”的随机外观,我们需要使用像素级渲染。我们可以使用32位无符号缓冲区来优化此过程,而不是8位,我们还可以关闭更近期的浏览器中的Alpha通道,这将加快整个过程(对于旧的浏览器,我们可以简单地为canvas元素设置一个黑色不透明背景)。

我们在主循环之外创建一个可重用的ImageData对象,因此主要成本仅为putImageData()而不是循环内部和外部的成本。

var ctx = c.getContext("2d", {alpha: false});       // context without alpha channel.
var idata = ctx.createImageData(c.width, c.height); // create image data
var buffer32 = new Uint32Array(idata.data.buffer);  // get 32-bit view

(function loop() {
  noise(ctx);
  requestAnimationFrame(loop)
})()

function noise(ctx) {
  var len = buffer32.length - 1;
  while(len--) buffer32[len] = Math.random() < 0.5 ? 0 : -1>>0;
  ctx.putImageData(idata, 0, 0);
}
/* for browsers wo/2d alpha disable support */
#c {background:#000}
<canvas id=c width=640 height=320></canvas>

一种非常高效的方法是,以一些内存为代价但降低了CPU成本,预先渲染一个更大的离屏画布,并仅使用 一次噪音,然后使用随机整数偏移将 画布放入主画布中。

它需要一些额外的准备步骤,但循环可以完全在GPU上运行。

var w = c.width;
var h = c.height;
var ocanvas = document.createElement("canvas");     // create off-screen canvas
ocanvas.width = w<<1;                               // set offscreen canvas x2 size
ocanvas.height = h<<1;

var octx = ocanvas.getContext("2d", {alpha: false});
var idata = octx.createImageData(ocanvas.width, ocanvas.height);
var buffer32 = new Uint32Array(idata.data.buffer);  // get 32-bit view

// render noise once, to the offscreen-canvas
noise(octx);

// main loop draw the offscreen canvas to random offsets
var ctx = c.getContext("2d", {alpha: false});
(function loop() {
  var x = (w * Math.random())|0;                    // force integer values for position
  var y = (h * Math.random())|0;
  
  ctx.drawImage(ocanvas, -x, -y);                   // draw static noise (pun intended)
  requestAnimationFrame(loop)
})()

function noise(ctx) {
  var len = buffer32.length - 1;
  while(len--) buffer32[len] = Math.random() < 0.5 ? 0 : -1>>0;
  ctx.putImageData(idata, 0, 0);
}
/* for browsers wo/2d alpha disable support */
#c {background:#000}
<canvas id=c width=640 height=320></canvas>

需要注意的是,使用后一种技术可能会存在“冻结”的风险,即新的随机偏移量与先前的偏移量相似。为了解决这个问题,在随机位置时设置条件,禁止连续太接近的位置。


@Mouseroot 是的,但使用8位数组需要迭代4倍的像素。因此问题变成了浏览器夹紧还是4倍迭代速度更有效率.. :) - user1693593
@Ken,你能否提供一个关于缓冲区的好资料链接吗?顺便说一句,我真的很喜欢你的实现方式,想更深入了解背后的思路。 - Ilan Biala
当我尝试移除new关键字时,我在Chromium中遇到了一个类型错误。Uncaught TypeError: Constructor Uint32Array requires 'new' - Mouseroot
1
我不明白...你在谈论性能优化,但是在每个迭代步骤中创建idatabuffer32。不需要进行基准测试就可以看出这比仅在循环之前(调整大小时)缓存/创建它们要慢得多。缓冲区长度也可以通过w*h计算 ;) - yckart
1
yckart是正确的。在Firefox(48.0.1)中,噪声动画每秒钟会卡顿一次(垃圾回收?)。将所有变量放在noise()之外后,可以正常运行。也就是说,对于1680x954,它仍然使用> 10%的CPU(i5-6500 CPU @ 3.20GHz)。 - handle
显示剩余14条评论

12

我之前尝试过制作类似的函数。我给每个像素设置了随机值,并且叠加了一个随着时间向上移动的正弦波,只是为了使其看起来更加逼真。您可以玩弄波中的常数以获得不同的效果。

var canvas = null;
var context = null;
var time = 0;
var intervalId = 0;

var makeNoise = function() {
  var imgd = context.createImageData(canvas.width, canvas.height);
  var pix = imgd.data;

  for (var i = 0, n = pix.length; i < n; i += 4) {
      var c = 7 + Math.sin(i/50000 + time/7); // A sine wave of the form sin(ax + bt)
      pix[i] = pix[i+1] = pix[i+2] = 40 * Math.random() * c; // Set a random gray
      pix[i+3] = 255; // 100% opaque
  }

  context.putImageData(imgd, 0, 0);
  time = (time + 1) % canvas.height;
}

var setup = function() {
  canvas = document.getElementById("tv");
  context = canvas.getContext("2d");
}

setup();
intervalId = setInterval(makeNoise, 50);
<canvas id="tv" width="400" height="300"></canvas>

我在一个网站上使用它作为预加载器。我还添加了一个音量控制器作为加载栏,这里有一个屏幕截图

电视噪声屏幕截图


10

我重新编写了你的代码,每个步骤都是单独的,这样你就可以重复使用而不必每次都创建和重新创建,减少了循环调用,希望能够清晰地阅读并理解。

function generateNoise(opacity, h, w) {
    function makeCanvas(h, w) {
         var canvas = document.createElement('canvas');
         canvas.height = h;
         canvas.width = w;
         return canvas;
    }

    function randomise(data, opacity) { // see prev. revision for 8-bit
        var i, x;
        for (i = 0; i < data.length; ++i) {
            x = Math.floor(Math.random() * 0xffffff); // random RGB
            data[i] =  x | opacity; // set all of RGBA for pixel in one go
        }
    }

    function initialise(opacity, h, w) {
        var canvas = makeCanvas(h, w),
            context = canvas.getContext('2d'),
            image = context.createImageData(h, w),
            data = new Uint32Array(image.data.buffer);
        opacity = Math.floor(opacity * 0x255) << 24; // make bitwise OR-able
        return function () {
            randomise(data, opacity); // could be in-place for less overhead
            context.putImageData(image, 0, 0);
            // you may want to consider other ways of setting the canvas
            // as the background so you can take this out of the loop, too
            document.body.style.backgroundImage = "url(" + canvas.toDataURL("image/png") + ")";
        };
    }

    return initialise(opacity || 0.2, h || 55, w || 55);
}

现在您可以创建一些间隔或超时循环,以保持重新调用生成的函数。
window.setInterval(
    generateNoise(.8, 200, 200),
    100
);

或者使用 requestAnimationFrame,就像在 Ken的回答中所述。

var noise = generateNoise(.8, 200, 200);

(function loop() {
    noise();
    requestAnimationFrame(loop);
})();

DEMO


为什么你会在函数内部定义函数? - Ilan Biala
@IlanBiala,闭包,将内部函数暴露给外部函数变量,想要更好的解释可以查找 Douglas Crawford 的《JavaScript权威指南》。 - Mouseroot
@IlanBiala 我其实只需要一个(返回的),但我写了很多,这样每个“步骤”都清晰明了。 - Paul S.

9

看起来Ken的回答还不错,但是经过观察一些真实电视静态视频后,我有了一些想法,下面是我想到的两个版本:

http://jsfiddle.net/2bzqs/

http://jsfiddle.net/EnQKm/

更改摘要:

  • 将每个像素单独分配颜色改为多个像素运行获取单个颜色,因此您会得到这些短暂的、变化大小的水平线段。
  • 我应用了一个gamma曲线(使用Math.pow)将颜色稍微偏向黑色。
  • 我不在“带状”区域内应用gamma以模拟带状现象。

以下是代码的主要部分:

var w = ctx.canvas.width,
    h = ctx.canvas.height,
    idata = ctx.createImageData(w, h),
    buffer32 = new Uint32Array(idata.data.buffer),
    len = buffer32.length,
    run = 0,
    color = 0,
    m = Math.random() * 6 + 4,
    band = Math.random() * 256 * 256,
    p = 0,
    i = 0;

for (; i < len;) {
    if (run < 0) {
        run = m * Math.random();
        p = Math.pow(Math.random(), 0.4);
        if (i > band && i < band + 48 * 256) {
            p = Math.random();
        }
        color = (255 * p) << 24;
    }
    run -= 1;
    buffer32[i++] = color;
}

6

我刚好写了一个脚本,可以通过获取黑色画布的像素并随机更改alpha值来实现这一功能,并使用putImageData函数。

结果可以在http://mouseroot.github.io/Video/index.html找到。

currentAnimationFunction变量的当前值为staticScreen

screenObject变量获取了ID为"screen"的元素的2D上下文。

pixels变量获取了从(0,0)开始,宽度和高度均为500的图像数据。

function staticScreen()
        {
            requestAnimationFrame(currentAnimationFunction);
            //Generate static
            for(var i=0;i < pixels.data.length;i+=4)
            {
                pixels.data[i] = 255;
                pixels.data[i + 1] = 255;
                pixels.data[i + 2] = 255;
                pixels.data[i + 3] = Math.floor((254-155)*Math.random()) + 156;
            }
            screenObject.putImageData(pixels,0,0,0,0,500,500);
            //Draw 'No video input'
            screenObject.fillStyle = "black";
            screenObject.font = "30pt consolas";
            screenObject.fillText("No video input",100,250,500);
        }

5
我的效果看起来虽然不是和真实的电视静态一模一样,但还是相似的。我只是在循环所有画布上的像素,并将每个随机坐标处像素的RGB颜色组件更改为随机颜色。这个演示可以在CodePen上找到。 代码如下:
// Setting up the canvas - size, setting a background, and getting the image data(all of the pixels) of the canvas. 
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
canvas.width = 400;
canvas.height = 400;
canvasData = ctx.createImageData(canvas.width, canvas.height);

//Event listeners that set the canvas size to that of the window when the page loads, and each time the user resizes the window
window.addEventListener("load", windowResize);
window.addEventListener("resize", windowResize);

function windowResize(){
  canvas.style.width = window.innerWidth + 'px';
  canvas.style.height = window.innerHeight + 'px';
}

//A function that manipulates the array of pixel colour data created above using createImageData() 
function setPixel(x, y, r, g, b, a){
  var index = (x + y * canvasData.width) * 4;

  canvasData.data[index] = r;
  canvasData.data[index + 1] = g;
  canvasData.data[index + 2] = b;
  canvasData.data[index + 3] = a;
}

window.requestAnimationFrame(mainLoop);

function mainLoop(){
  // Looping through all the colour data and changing each pixel to a random colour at a random coordinate, using the setPixel function defined earlier
  for(i = 0; i < canvasData.data.length / 4; i++){
    var red = Math.floor(Math.random()*256);
    var green = Math.floor(Math.random()*256);
    var blue = Math.floor(Math.random()*256);
    var randX = Math.floor(Math.random()*canvas.width); 
    var randY = Math.floor(Math.random()*canvas.height);

    setPixel(randX, randY, red, green, blue, 255);
  }

  //Place the image data we created and manipulated onto the canvas
  ctx.putImageData(canvasData, 0, 0);

  //And then do it all again... 
  window.requestAnimationFrame(mainLoop);
}

我认为,对于真正的随机性来说,没有意义去随机化正在被写入的像素,因为该值已经是随机的。 - handle

4
您可以像这样执行:
window.setInterval('generateNoise(.8)',50);
第二个参数50是毫秒级的延迟。增加50会使动画变慢,减少则相反。
不过这种方式会严重影响网页性能。如果是我,我会在服务器端进行渲染并输出一些帧迭代作为动态gif。虽然不能实现无限随机性,但这将极大地提高性能,并且我认为大多数人甚至不会注意到这一点。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接