【问题标题】:Repeat HTML canvas element (box) to fill whole viewport重复 HTML 画布元素(框)以填充整个视口
【发布时间】:2021-03-28 03:09:28
【问题描述】:

我有一个动画画布来在背景上创建噪音效果。我的代码的简短解释如下:使用for 循环,我在画布周围随机绘制 1px X 1px 正方形(点)。它会产生静态噪音(颗粒)。后来,我用requestAnimationFrame为它制作动画。

我的问题是,如果我将画布的原始大小设置为整个视口,浏览器将无法处理巨大的绘图空间,并且动画变得非常缓慢。所以我需要保持画布的原始尺寸很小,比如 50px 并在整个视口上重复它。

我知道我可以轻松使用document.body.style.backgroundImage,它本身会自动重复元素,但它不符合我的业务逻辑。我需要这个画布元素来覆盖页面上的任何其他元素并保持透明。仅仅设置不透明度也不适合我。

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

【问题讨论】:

  • 我会创建一个小 gif 并将其用作背景

标签: javascript html css html5-canvas css-animations


【解决方案1】:

在画布上生成噪点(通常以像素级别工作)的最佳方法是使用ImageData
一个一个地绘制每个像素会非常慢(需要向 GPU 发送大量指令),而在 TypedArray 上的简单循环则非常快。

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 }
&lt;canvas&gt;&lt;/canvas&gt;

如果仍然太慢,您也可以只为画布大小的一部分生成噪波并多次绘制:

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 }
&lt;canvas&gt;&lt;/canvas&gt;

【讨论】:

  • 这很棒。它正是我想要的。谢谢。但在实际使用它之前,我试图更深入地研究它以更好地理解,不,我很卡住。我发布了另一个问题,因为它超出了这个问题的范围,所以也许你可以分享你的意见? stackoverflow.com/questions/65361623/…
【解决方案2】:

愚弄眼睛。

当以 60fps 呈现信息时,您可以很容易地欺骗人眼。

Visual FX 不应该影响性能,因为视觉 FX 是次要的展示,不如主要内容重要,它应该占据 CPU 周期的大部分份额。

sn-p 的运行渲染成本为每帧 1 个fillRect 调用,以 CPU 周期换取内存。我认为现在的问题是它太快了,也许您可​​能希望将速率降低到 30fps(请参阅选项)。

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>

最佳浏览整页。

它是如何工作的

函数createNoise(size, count, pow) 创建count 随机噪声模式。图案的分辨率为size。该函数返回一个函数,当使用 2D 上下文调用该函数时,该函数会使用其中一种随机模式填充上下文。

图案偏移是随机设置的,也是沿两个轴随机镜像的。还有一个快速测试可确保同一图案不会连续绘制两次(超过所需的帧速率)。

最终效果是随机噪声,人眼几乎无法将其与真正的随机噪声区分开来。

返回的函数有第二个 [可选] 参数,如果为 true,则重绘与前一帧相同的模式,从而允许噪波效果的帧速率独立于任何正在渲染的内容,无论是在噪波之下还是之上。

模拟真实噪声

createNoise(size, count, pow)[pow] 的第三个参数是可选的。

在胶片和老式模拟 CRT 上看到的噪声分布不均。高能(更亮)噪声比低能噪声要少得多。

参数pow 控制噪声的分布。值 1 具有均匀分布(每个能级具有相同的发生机会)。值 > 1 会降低分布的高能侧的几率。我个人会使用 8 的值,但这当然是个人喜好问题。

片段选项

为了帮助评估此方法,请单击画布循环预设。预设修改随机模式的数量、每个模式的分辨率、噪声功率和噪声的帧率(注意帧率为 60FPS,噪声帧率与基本速率无关)

与您的代码最匹配的设置是第一个(分辨率 128 x 128 10 帧,功率:1,帧速率 60)

请注意,当图案的分辨率在64或以下时,您可以开始看到重复的图案,即使添加许多随机帧也不会降低此FX

可以改进吗

是的,但您需要进入 WebGL 的世界。与 2D fillRect 填充画布相比,WebGL 着色器可以更快地渲染此效果。

WebGL 不只是 3D,它适用于任何有像素的东西。如果您进行大量视觉工作,那么花时间学习 WebGL 是非常值得的,因为它通常可以通过 2D API 实现不可能的事情。

WebGL / Canvas 2D 混合解决方案

以下是使用 WebGL 的快速破解解决方案。 (因为我怀疑你会使用这种方法,所以我没有看到投入太多时间的意义)

它已被构建为与 2D API 兼容。呈现的 WebGL 内容以图像的形式呈现,然后您只需将其呈现在 2D API 内容的顶部。

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】:

还有来自未来的想法,使用 element()。实际上它只适用于 Firefox

element() 函数允许作者将文档中的元素用作图像。当引用的元素改变外观时,图像也会改变。例如,这可用于在幻灯片中创建下一张/上一张幻灯片的实时预览,或引用画布元素以生成精美的渐变甚至动画背景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.

【讨论】:

  • 不幸的是,正如您自己提到的,它不适用于我的 Chrome。另外,我相信使用画布作为图像不会产生透明的效果。我需要将这种噪点效果放在文本和按钮之上,而不是在其后面,就像使用画布作为背景图像时一样。使用背景图片,它要么完全在后面,要么完全在顶部,覆盖其他所有内容。
  • 尽管它仍在草稿中,但我不会在这方面打赌,它会得到广泛的支持。由于性能问题,IIRC 甚至 FF 都计划放弃它。
  • @Demiko 我的示例实际上是在创建您正在寻找的透明效果,它位于所有元素的顶部(注意使用位置:固定、z-index 和不透明度)
  • @TemaniAfif 对不起,我误导了你。你在做什么是正确的。这实际上不是关于使用画布作为背景图像或将孩子附加到正文,而不是关于透明度。我正在实现画布在其他元素上的完美过渡。我无法实现的是重复附加的孩子(画布)。 document.body.style.backgroundImage 包含 repeat 作为简写。而且我猜 -moz-element() 也是隐式执行的,但是 appendChild 需要显式重复。我希望我不会让任何人感到困惑。
【解决方案4】:

您可以创建一个与图块大小相同的屏幕外画布,例如50x50,不要将其附加到 DOM 并在其上生成随机噪声。可以使用以下方法检索此画布的像素数据: data=getImageData(0, 0, offScreenCanvas.width, offScreenCanvas.height);

然后可以使用此数据进一步将其绘制到您的屏幕画布上

putImageData(data, x, y);

因此,如果您使用 for 循环将该数据放置在 x 和屏幕外画布大小的倍数 - 50 - 您可以使用此图块填充整个画布。

一个问题是您会在这些瓷砖之间看到明显的接缝。作为补偿,我会添加另一个 for 循环并在某个随机位置绘制该图块。

这是一个例子:

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

【讨论】:

    猜你喜欢
    • 2016-09-15
    • 2016-01-09
    • 1970-01-01
    • 2016-02-18
    • 2013-03-25
    • 1970-01-01
    • 2021-04-24
    • 2015-08-07
    • 2016-02-24
    相关资源
    最近更新 更多