65

I have a function named generateNoise() which creates a canvas element and paints random RGBA values to it; which, gives the appearance of noise.


My Question

What would be the best way to infinitely animate the noise to give the appearance of movement. So that it may have more life?


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);
Armeen Moon
  • 18,061
  • 35
  • 120
  • 233
  • 1
    `window.setInterval('generateNoise(.8)',50);` – CrayonViolent Feb 25 '14 at 02:39
  • 1
    You can reduce your `Math.random` and `Math.floor` calls by doing something like `x = Math.floor(Math.random() * 0xffffff); r = x & 0xff; g = (x & 0xff00) >>> 8; b = (x & 0xff0000) >>> 16;` – Paul S. Feb 25 '14 at 02:45
  • Some tips: 1) `!!!document.createElement('canvas').getContext` to `!document.createElement('canvas').getContext` 2) set up if/else so that users who don't have canvas won't get errors 3) put spaces between commas and text after, and space between `(param) {` for functions – Ilan Biala Feb 25 '14 at 03:02
  • @PaulS. do I make that work? i'm totally a nerb? it looks correct but I cant get it to work; new colors: r = Math.floor(Math.random() * 155); g = Math.floor(Math.random() * 255); b = Math.floor(Math.random() * 155); – Armeen Moon Feb 25 '14 at 03:04
  • 1
    also all i had to add was `requestAnimationFrame(generateNoise);` and it does what you need place it in the first line in the function. http://jsfiddle.net/eS9cc/ – Mouseroot Feb 25 '14 at 03:08
  • Also avoid repainting a canvas very often, it is resource-intensive. If you can use the [improved for loop](http://stackoverflow.com/questions/17484227/javascript-improved-native-for-loop) that will help. Set up a canvas that is only 10px, and scatter that, then you will save a lot of painting. Check Chrome Developer Tools or Firebug for performance stuff. Something that could make it look alive is using a css animation to really quickly move it would be good. Check out my [pen](http://codepen.io/ilanbiala/pen/CgGIw) and see what I mean. You can play around with the values and timing. – Ilan Biala Feb 25 '14 at 03:09
  • @IlanBiala would you do the math.random() fixes that Paul S. said and put an offical answer? – Armeen Moon Feb 25 '14 at 03:11
  • @llan Biala, that is an intersting way todo it, however it looks a little wierd not like actual noise. – Mouseroot Feb 25 '14 at 03:14
  • @Mouseroot would you know why when I do it yoru way opacity doesnt work? – Armeen Moon Feb 25 '14 at 03:17
  • it could be that your iterating over the x,y pixel positions while im itering over each pixel and adding 4 for r,g,b and alpha and im using 255 (black) for the r,g,b and a random value between 155 and 254, also im directly altering the pixels while your placing each individual pixel. so my loop alters all the pixels in the array and then applying it, while your is altering each pixel and setting it. – Mouseroot Feb 25 '14 at 03:21
  • @Mouseroot Nice. RAF is awesome, but you could probably achieve the same thing with css animation on the body background position. I think RAF is also not fully supported yet as per http://caniuse.com/#feat=requestanimationframe – Ilan Biala Feb 25 '14 at 03:34
  • @MatthewHarwood you want a demo of mine or something else? – Ilan Biala Feb 25 '14 at 03:37
  • its not fully supported in all browsers but there exists a polyfill for browsers that still rely on vender prefixes and that fallback on setInterval for browsers that just flat out dont support rAF. – Mouseroot Feb 25 '14 at 03:42

7 Answers7

80

Update 1/2017: I rewrote the entire answer as it started to become rather messy, and to address some of the issues pointed out in the comments. The original answer can be found here. The new answer has in essence the same code but improved, and with a couple of new techniques, one utilizes a new feature available since this answer was first posted.


For a "true" random look we would need to use pixel-level rendering. We can optimize this using 32-bit unsigned buffers instead of 8-bit, and we can also turn off the alpha-channel in more recent browsers which speeds up the entire process (for older browsers we can simply set a black opaque background for the canvas element).

We create a reusable ImageData object once outside the main loop so the main cost is only putImageData() and not both inside the loop.

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>

A very efficient way, at the cost of some memory but reduced cost on the CPU, is to pre-render a larger off-screen canvas with the noise once, then place that canvas into the main one using random integer offsets.

It require a few extra preparation steps but the loop can run entirely on the 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>

Do note though that with the latter technique you may risk getting "freezes" where the new random offset is similar to the previous one. To work around this problem, set criteria for the random position to disallow too close positions in a row.

anthonygood
  • 402
  • 4
  • 8
  • it should be noted that you could use a `Uint8ClampedArray` so you can do `pixels[i+4] *= alpha;` without having to worry about manual clamping as shown in this html5rocks post http://www.html5rocks.com/en/tutorials/webgl/typed_arrays/ – Mouseroot Feb 25 '14 at 03:26
  • `requestAnimationFrame` is amazing and I always forget about it. This seems to fill the whole page rather than setting a background (but that wasn't enough to prevent me `+1`ing). – Paul S. Feb 25 '14 at 03:27
  • @Mouseroot yes, but with a 8-bit array you need to iterate 4x as many pixels. So the question becomes is the browser clamping or the 4x iteration speed more efficient.. :) –  Feb 25 '14 at 03:28
  • @PaulS. yes, rAF is pretty neat! :) You could BTW toggle the frame rate to 30 fps (the old TVs had 50/60 hz interlaced or 25/30 FPS depending on PAL/NTSC). That would also save some resources. –  Feb 25 '14 at 03:29
  • @Ken doing `i = i + 4` would give you exactly the same number of iterations and you don't need to `new`. Doing it your with the 32 bit array way might have been more efficient for my answer though as I spit the bits the other way around – Paul S. Feb 25 '14 at 03:29
  • @PaulS. hmm, you could be right about that here.. :-| Though, there is a loop-up factor involved. Need to test... The new just creates a reference to the internal buffer though (the same array buffer is used for both). –  Feb 25 '14 at 03:32
  • @Ken, thats a good question I would assume, clamping may be faster, but thats best left to a javascript engine engineer, or perhaps someone could create a jspref to test it? – Mouseroot Feb 25 '14 at 03:32
  • @Mouseroot we should really test it for future references (or at least for the fun of it) :-) –  Feb 25 '14 at 03:33
  • @Ken Can you link to a good source on buffers? I really like your implementation btw and would like to know more about the thought behind it. – Ilan Biala Feb 25 '14 at 03:35
  • when I tried to remove the `new` keyword i got a typeerror in chromium. `Uncaught TypeError: Constructor Uint32Array requires 'new' ` – Mouseroot Feb 25 '14 at 03:36
  • @Mouseroot Ok guys, the winner is... http://jsperf.com/iterate-8-vs-32-bits-typed-array (seems to be 32-bit buffer on my machine FF and Canary) –  Feb 25 '14 at 03:40
  • @Mouseroot you will need the new keyword. It creates a setup to make sure the value written to the array buffer is correct, but the buffer is the same (given as a reference in the constructor). –  Feb 25 '14 at 03:41
  • @IlanBiala I just made this one from the top of my head, but I remember Html5rocks has some good stuff on typed arrays: http://www.html5rocks.com/en/tutorials/webgl/typed_arrays/ –  Feb 25 '14 at 03:44
  • Unfortunately, this isn't working in my browser (just so you know): Windows 8.1, IE 11. – uSeRnAmEhAhAhAhAhA Feb 25 '14 at 15:21
  • @user-12506 did you get any error message in console? –  Feb 25 '14 at 16:00
  • I also noticed this didn't work in IE 11. I've gotten an error in the console: `Typed array constructor argument is invalid` @ `new Uint32Array(noisePatterns[noisePatterns.length - 1].data.buffer)` – roydukkey Jun 10 '14 at 16:06
  • 1
    I don't get the point... You are talking about *performance optimization* but create the `idata` and `buffer32` on each iteration step. There's no benchmark required to see that this is much slower, than caching/creating them just before the loop (on resize). The buffer length could be also calculated by `w*h` ;) – yckart Mar 20 '16 at 17:16
  • 1
    yckart's right. In Firefox (48.0.1), the noise animation hangs every second or so (garbage colletion?). [Runs fine](http://jsfiddle.net/vX7NK/347/) once all the variables are outside of noise(). That is, it still uses > 10% CPU (i5-6500 CPU @ 3.20GHz) for 1680x954. – handle Aug 19 '16 at 17:59
  • Strangely the demo works in Firefox 111 but not Chrome 111. – Aidan Feldman Mar 25 '23 at 05:57
12

I tried to make a similar function a while ago. I set each pixel random value, and in addition to that, I overlayed a sinusodial wave that traveled upwards with time just to make it look more realistic. You can play with the constants in the wave to get different effects.

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>

I used it as a preloader on a site. I also added a volume rocker as a loading bar, here's a screenshot:

TV noise screenshot

John Bupit
  • 10,406
  • 8
  • 39
  • 75
9

I re-wrote your code so each step is separate so you can re-use things without having to create and re-create each time, reduced in-loop calls and hopefully made it clear enough to be able to follow by reading it.

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

Now you can create some interval or timeout loop which keeps re-invoking the generated function.

window.setInterval(
    generateNoise(.8, 200, 200),
    100
);

Or with requestAnimationFrame as in Ken's answer

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

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

DEMO

Community
  • 1
  • 1
Paul S.
  • 64,864
  • 9
  • 122
  • 138
  • why do you have functions inside functions? – Ilan Biala Feb 25 '14 at 03:36
  • @IlanBiala, closures, to expose the inner function to the outer function variables, for a really good explenation look up Douglas Crawford `javascript, the good parts` – Mouseroot Feb 25 '14 at 03:38
  • @IlanBiala I only really needed to have one (that gets returned) but I wrote many so each "step" is clear. – Paul S. Feb 25 '14 at 03:40
9

Ken's answer looked pretty good, but after looking at some videos of real TV static, I had some ideas and here's what I came up with (two versions):

http://jsfiddle.net/2bzqs/

http://jsfiddle.net/EnQKm/

Summary of changes:

  • Instead of every pixel being independently assigned a color, a run of multiple pixels will get a single color, so you get these short, variable-sized horizontal lines.
  • I apply a gamma curve (with the Math.pow) to bias the color toward black a little.
  • I don't apply the gamma in a "band" area to simulate the banding.

Here's the main part of the code:

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;
}
FogleBird
  • 74,300
  • 25
  • 125
  • 131
6

I happen to have just written a script that does just this, by getting the pixels from a black canvas and just altering random alpha values and using putImageData

Result can be found at http://mouseroot.github.io/Video/index.html

var currentAnimationFunction = staticScreen

var screenObject = document.getElementById("screen").getContext("2d");

var pixels = screenObject.getImageData(0,0,500,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);
        }
Mouseroot
  • 1,034
  • 2
  • 9
  • 13
5

Mine doesn't look identical to real TV static, but it's similar nonetheless. I'm just looping through all the pixels on canvas, and changing the RGB colour components of each pixel at a random coordinate to a random colour. The demo can be found over at CodePen.

The code is as follows:

// 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);
}
  • I think that for true randomness, it would not make sense to randomize the pixel that is being written, since the value is random already. – handle Aug 19 '16 at 18:05
4

You can do it like this:

window.setInterval('generateNoise(.8)',50);

The 2nd arg 50 is a delay in milliseconds. Increasing 50 will slow it down and decreasing visa versa.

though.. this is going to severely affect web page performance. If it were me, I'd do the rendering server-side and render a handful of frame iterations and output as an animated gif. Not quite the same as infinite randomness, but would be a huge performance boost and IMO most people won't even notice.

CrayonViolent
  • 32,111
  • 5
  • 56
  • 79