Repeat HTML canvas element (box) to fill whole viewport

自作多情 提交于 2021-02-10 13:11:11

问题


I have an animated canvas to create a noise effect on the background. Short explanation of my code is following: using for loop I am drawing 1px X 1px squares (dots) around canvas at random. It creates a static noise (grain). Later, I animate it with requestAnimationFrame.

My problem is that if I set the original size of the canvas to whole viewport, the browser can not handle huge drawing space and animation becomes extremely sluggish. So I need to keep the original size of canvas small, like 50px and repeat it on whole viewport.

I know I can easily use document.body.style.backgroundImage which on itself repeats the element automatically, but it does not fit my business logic. I need this canvas element to layover any other element on the page and be transparent. Just setting opacity does not fit me as well.

var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.className = "canvases";
canvas.width = 500;
canvas.height = 500;
document.body.appendChild(canvas);

function generateNoise() {
  window.requestAnimationFrame(generateNoise);
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  let x;
  let y;
  let rgb;

  for (x = 0; x < canvas.width; x++) {
    for (y = 0; y < canvas.height; y++) {
      rgb = Math.floor(Math.random() * 255);
      ctx.fillStyle = `rgba(${rgb}, ${rgb}, ${rgb}, 0.2`;
      ctx.fillRect(x, y, 1, 1);
    }
  }

  canvas.setAttribute("style", "position: absolute;");
  canvas.style.zIndex = 1;
  document.body.appendChild(canvas);
}

generateNoise();
.canvases {
  margin: 50px;
  border: 1px solid black;
}

回答1:


The best to generate noise on a canvas (and to work at a pixel level in general) is to use an ImageData.
Painting every pixel one by one will be extremely slow (a lot of instructions to the GPU), while a simple loop over a TypedArray is quite fast.

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
let image; // using let because we may change it on resize

onresize = (evt) => {
  const width = canvas.width = window.innerWidth;
  const height = canvas.height =window.innerHeight;
  image = new ImageData(width, height);
};
onresize();
const anim = (t) => {
  const arr = new Uint32Array(image.data.buffer);
  for( let i = 0; i < arr.length; i++ ) {
    // random color with fixed 0.2 opacity
    arr[i] = (Math.random() * 0xFFFFFF) + 0x33000000;
  }
  ctx.putImageData(image, 0, 0);
  requestAnimationFrame(anim);
};
anim();
canvas { position: absolute; top: 0; left: 0 }
<canvas></canvas>

If it's still too slow, you could also generate the noise only for a portion of the canvas size and draw it multiple times:

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
let image; // using let because we may change it on resize

onresize = (evt) => {
  const width = canvas.width = window.innerWidth;
  const height = canvas.height =window.innerHeight;
  image = new ImageData(width / 2, height / 2);
};
onresize();
const anim = (t) => {
  ctx.clearRect(0,0,canvas.width,canvas.height);
  const arr = new Uint32Array(image.data.buffer);
  for( let i = 0; i < arr.length; i++ ) {
    // random color with fixed 0.2 opacity
    arr[i] = (Math.random() * 0xFFFFFF) + 0x33000000;
  }
  const width = image.width;
  const height = image.height;
  ctx.putImageData(image, 0, 0);
  ctx.drawImage(canvas, 0, 0, width, height, width, 0, width, height);
  ctx.drawImage(canvas, 0, 0, width * 2, height, 0, height, width * 2, height);

  requestAnimationFrame(anim);
};
anim();
canvas { position: absolute; top: 0; left: 0 }
<canvas></canvas>



回答2:


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



回答3:


And idea from the future using element(). Actually it works only on Firefox

The element() function allows an author to use an element in the document as an image. As the referenced element changes appearance, the image changes as well. This can be used, for example, to create live previews of the next/previous slide in a slideshow, or to reference a canvas element for a fancy generated gradient or even an animated background. ref

var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.id = "canvases";
canvas.width = 100;
canvas.height = 100;
document.body.appendChild(canvas);

function generateNoise() {
  window.requestAnimationFrame(generateNoise);
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  let x;
  let y;
  let rgb;

  for (x = 0; x < canvas.width; x++) {
    for (y = 0; y < canvas.height; y++) {
      rgb = Math.floor(Math.random() * 255);
      ctx.fillStyle = `rgba(${rgb}, ${rgb}, ${rgb}, 0.2`;
      ctx.fillRect(x, y, 1, 1);
    }
  }

  document.querySelector('.hide').appendChild(canvas);
}

generateNoise();
.hide {
  height:0;
  overflow:hidden;
}

body::before {
  content:"";
  position:fixed;
  z-index:9999;
  top:0;
  left:0;
  right:0;
  bottom:0;
  pointer-events:none;
  opacity:0.8;
  background:-moz-element(#canvases); /* use the canvas as background and repeat it */
}
<div class="hide"></div>

<h1>a title</h1>

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nec sagittis purus. Integer eget ante nec nisi malesuada vehicula. Vivamus urna velit, sodales in consequat ac, elementum id felis. Nulla vitae interdum eros. Aliquam non enim leo. Nunc blandit odio ut lectus egestas, nec luctus dui blandit. Suspendisse sed magna vel lorem mattis pretium sit amet in quam. Aenean varius elementum massa at gravida.



回答4:


You can create an offscreen canvas the size of your tile e.g. 50x50, don't append it to the DOM and generate the random noise on it. The pixel data of this canvas can be retrieved using: data=getImageData(0, 0, offScreenCanvas.width, offScreenCanvas.height);

This data then can be further used to be drawn onto your on-screen canvas using

putImageData(data, x, y);

So if you use a for-loop to put that data at x & in multiples of your offscreen canvas size - 50 - you can fill the whole canvas using this tile.

One problem is that you will see an obvious seam in-between those tiles. To compensate I would add another for loop and draw that tile at some random spot.

Here's an example:

var canvas = document.createElement("canvas");
var offScreenCanvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
var offCtx = offScreenCanvas.getContext("2d");
canvas.className = "canvases";
canvas.width = 500;
canvas.height = 500;
offScreenCanvas.width = 50;
offScreenCanvas.height = 50;
document.body.appendChild(canvas);
canvas.setAttribute("style", "position: absolute;");
canvas.style.zIndex = 1;

function generateNoise() {
  window.requestAnimationFrame(generateNoise);
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  let x;
  let y;
  let rgb;

  for (x = 0; x < offScreenCanvas.width; x++) {
    for (y = 0; y < offScreenCanvas.height; y++) {
      rgb = Math.floor(Math.random() * 255);
      offCtx.fillStyle = `rgba(${rgb}, ${rgb}, ${rgb}, 0.2`;
      offCtx.fillRect(x, y, 1, 1);
    }
  }
  var data = offCtx.getImageData(0, 0, offScreenCanvas.width, offScreenCanvas.height);

  for (x = 0; x < canvas.width; x += offScreenCanvas.width) {
    for (y = 0; y < canvas.height; y += offScreenCanvas.height) {

      ctx.rotate(parseInt(Math.random() * 4) * 90 * Math.PI / 180);

      ctx.putImageData(data, x, y);
      ctx.setTransform(1, 0, 0, 1, 0, 0);
    }
  }

  for (a = 0; a < 40; a++) {
    ctx.putImageData(data, parseInt(Math.random() * canvas.width), parseInt(Math.random() * canvas.height));

  }

}

generateNoise();
.canvases {
  margin: 50px;
  border: 1px solid black;
}


来源:https://stackoverflow.com/questions/65340502/repeat-html-canvas-element-box-to-fill-whole-viewport

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!