Fooling the eye.
You can fool the human eye quite easily when presenting information at 60fps.
Visual FXs should never compromise performance as the visual FX is a secondary presentation less important than the primary content which should get the lions share of the CPU cycles.
The snippet has a running render cost of 1 fillRect
call per frame, exchanging CPU cycles for memory. I think that now the problem is that it is too fast and maybe you may want to reduce the rate down to 30fps (see options).
var ctx = canvas.getContext("2d");
var noiseIdx = 0
const noiseSettings = [
[128, 10, 1, 1],
[128, 10, 2, 1],
[128, 10, 4, 1],
[128, 10, 8, 1],
[128, 10, 1, 2],
[128, 10, 8, 2],
[256, 20, 1, 1],
[256, 20, 1, 2],
[32, 30, 1, 1],
[64, 20, 1, 1],
];
var noise, rate, frame = 0;
setNoise();
function setNoise() {
const args = noiseSettings[noiseIdx++ % noiseSettings.length];
noise = createNoise(...args);
info.textContent = "Click to cycle. Res: " + args[0] + " by " + args[0] + "px " + args[1] +
" frames. Noise power: " + args[2] + " " + (60 / args[3]) + "FPS";
rate = args[3];
}
mainLoop();
function mainLoop() {
if (ctx.canvas.width !== innerWidth || ctx.canvas.height !== innerHeight) {
canvas.width = innerWidth;
canvas.height = innerHeight;
} else {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
frame++;
noise(ctx, (frame % rate) !== 0);
requestAnimationFrame(mainLoop);
}
canvas.addEventListener("click", setNoise);
function createNoise(size, frameCount, pow = 1) {
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const frames = [];
while (frameCount--) { frames.push(createNoisePattern(canvas)) }
var prevFrame = -1;
const mat = new DOMMatrix();
return (ctx, hold = false) => {
if (!hold || prevFrame === -1) {
var f = Math.random() * frames.length | 0;
f = f === prevFrame ? (f + 1) % frames.length : f;
mat.a = Math.random() < 0.5 ? -1 : 1;
mat.d = Math.random() < 0.5 ? -1 : 1;
mat.e = Math.random() * size | 0;
mat.f = Math.random() * size | 0;
prevFrame = f;
frames[f].setTransform(mat);
}
ctx.fillStyle = frames[prevFrame];
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
function createNoisePattern(canvas) {
const ctx = canvas.getContext("2d");
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const d32 = new Uint32Array(imgData.data.buffer);
const alpha = (0.2 * 255) << 24;
var i = d32.length;
while (i--) {
const r = Math.random()**pow * 255 | 0;
d32[i] = (r << 16) + (r << 8) + r + alpha;
}
ctx.putImageData(imgData, 0, 0);
return ctx.createPattern(canvas, "repeat")
}
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
<div id="info"></div>
Best viewed full page.
How does it work
The function createNoise(size, count, pow)
creates count
patterns of random noise. The resolution of the pattern is size
. The function returns a function that when called with a 2D context fills the context with one of the random patterns.
The patterns offset is set randomly, and is also randomly mirrored along both axis. There is also a quick test to ensure that the same pattern is never drawn twice (over the desired frame rate) in a row.
The end effect is random noise that is near impossible for the human eye to distinguish from true random noise.
The returned function has a second [optional] argument that if true redraws the same pattern as the previous frame allowing the frame rate of the noise effect to be independent of any content being rendered, under or over the noise.
Simulating real noise
The 3rd argument of createNoise(size, count, pow)
, [pow]
is optional.
Noise as seen on film and older analog CRTs is not evenly distributed. High energy (brighter) noise is much rarer then low energy noise.
The argument pow
controls the distribution of noise. A value of 1 has an even distribution (each energy level has the same chance of occurring). Value > 1 decrease the odds of at the high energy side of the distribution. Personally I would use a value of 8 but that is of course a matter of personal taste.
Snippet Options
To help evaluate this method click the canvas to cycle the presets. The presets modify the number of random patterns, the resolution of each pattern, the noise power, and the frame rate of the noise (Note frame rate is 60FPS the noise frame rate is independent of the base rate)
The setting that most closely matches your code is the first (res 128 by 128 10 frames, power: 1, frame rate 60)
Note that when the resolution of the pattern is at or below 64 you can start to see the repeated pattern, even adding many random frames does not reduce this FX
Can this be improved
Yes but you will need to step into the world of WebGL. A WebGL shader can render this effect quicker than the 2D fillRect
can fill the canvas.
WebGL is not just 3D, it is for anything with pixels. It is well worth investing time into learning WebGL if you do a lot of visual work as it can generally make possible what is impossible via the 2D API.
WebGL / Canvas 2D hybrid solution
The following is a quick hack solution using WebGL. (As I doubt you will use this method I did not see the point of putting in too much time)
It has been built to be compatible with the 2D API. The rendered WebGL content is presented as an image that you then just render over the top of the 2D API content.
var w, h, cw, ch, canvas, ctx, globalTime,webGL;
const NOISE_ALPHA = 0.5;
const NOISE_POWER = 1.2;
const shadersSource = {
VertexShader : {
type : "VERTEX_SHADER",
source : `
attribute vec2 position;
uniform vec2 resolution;
varying vec2 texPos;
void main() {
gl_Position = vec4((position / resolution) * 2.0 - 1.0, 0.0, 1.0);
texPos = gl_Position.xy;
}`
},
FragmentShader : {
type : "FRAGMENT_SHADER",
source : `
precision mediump float;
uniform float time;
varying vec2 texPos;
const float randC1 = 43758.5453;
const vec3 randC2 = vec3(12.9898, 78.233, 151.7182);
float randomF(float seed) {
return pow(fract(sin(dot(gl_FragCoord.xyz + seed, randC2)) * randC1 + seed), ${NOISE_POWER.toFixed(4)});
}
void main() {
gl_FragColor = vec4(vec3(randomF((texPos.x + 1.01) * (texPos.y + 1.01) * time)), ${NOISE_ALPHA});
}`
}
};
var globalTime = performance.now();
resizeCanvas();
startWebGL(ctx);
ctx.font = "64px arial black";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
function webGLRender(){
var gl = webGL.gl;
gl.uniform1f(webGL.locs.timer, globalTime / 100 + 100);
gl.drawArrays(gl.TRIANGLES, 0, 6);
ctx.drawImage(webGL, 0, 0, canvas.width, canvas.height);
}
function display(){
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = "red";
ctx.fillText("Hello world "+ (globalTime / 1000).toFixed(1), cw, ch);
webGL && webGLRender();
}
function update(timer){ // Main update loop
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update);
}
requestAnimationFrame(update);
// creates vertex and fragment shaders
function createProgramFromScripts( gl, ids) {
var shaders = [];
for (var i = 0; i < ids.length; i += 1) {
var script = shadersSource[ids[i]];
if (script !== undefined) {
var shader = gl.createShader(gl[script.type]);
gl.shaderSource(shader, script.source);
gl.compileShader(shader);
shaders.push(shader);
}else{
throw new ReferenceError("*** Error: unknown script ID : " + ids[i]);
}
}
var program = gl.createProgram();
shaders.forEach((shader) => { gl.attachShader(program, shader); });
gl.linkProgram(program);
return program;
}
function createCanvas() {
var c,cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
function resizeCanvas() {
if (canvas === undefined) { canvas = createCanvas() }
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
setGlobals && setGlobals();
}
function setGlobals(){
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
// setup simple 2D webGL
function startWebGL(ctx) {
webGL = document.createElement("canvas");
webGL.width = ctx.canvas.width;
webGL.height = ctx.canvas.height;
webGL.gl = webGL.getContext("webgl");
var gl = webGL.gl;
var program = createProgramFromScripts(gl, ["VertexShader", "FragmentShader"]);
gl.useProgram(program);
var positionLocation = gl.getAttribLocation(program, "position");
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0.0, 0.0,1.0, 0.0,0.0, 1.0,0.0, 1.0,1.0, 0.0,1.0, 1.0]), gl.STATIC_DRAW);
var resolutionLocation = gl.getUniformLocation(program, "resolution");
webGL.locs = {
timer: gl.getUniformLocation(program, "time"),
};
gl.uniform2f(resolutionLocation, webGL.width, webGL.height);
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
setRectangle(gl, 0, 0, ctx.canvas.width, ctx.canvas.height);
}
function setRectangle(gl, x, y, width, height) {
var x1 = x;
var x2 = x + width;
var y1 = y;
var y2 = y + height;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([x1, y1, x2, y1, x1, y2, x1, y2, x2, y1, x2, y2]), gl.STATIC_DRAW);
}